//
// Created by Administrator on 2021/11/18.
// 用极坐标系生成一个球面网格，并且渲染它
// 先构建上下两个顶点，之后根据水平分割数和垂直分割数，逆时针方向增加theta和phi的值，转换为笛卡尔坐标系，构造三角形
//

#include "Application.h"
#include "MeshUtility.h"
#include "ShaderProgram.h"
#include "Geometry.hpp"
#include "Camera.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#define OUTLINE
#undef OUTLINE

class App : public Application
{
    struct PerDrawBuffer
    {
    public:
        Mat4x4f modelMatrix;
        Mat4x4f viewMatrix;
        Mat4x4f projMatrix;
        Vec3f cameraPos;
    };

    struct DirLight
    {
    public:
        Vec3f color;
        Vec3f direction;
    };

    struct PointLight
    {
    public:
        Vec3f color;
        Vec3f position;
        float range;
    };

    struct SpotLight
    {
    public:
        Vec3f color;
        float range;
        Vec3f position;
        float innerCos;
        Vec3f direction;
        float outerCos;
    };

    struct Material
    {
    public:
        Vec3f diffuseColor;
        Vec3f specularColor;
        float specularStrength;
    };

public:
    App() : Application("Test", 800, 600) {}

protected:
    void OnCreate() override
    {
        // 水平/垂直都用同一个
        static constexpr int N = 24;
        // 半径
        static constexpr float R = 3.0f;
        Vertex* vertices;
        GLuint* indices;
        int vertexCount;
        MeshUtility::GenerateSphere(N, R, vertices, indices, vertexCount, m_indexCount);
        // Shader
        m_sp = ShaderProgram("../../SphereMesh/shaders/vs.glsl",
                             "../../SphereMesh/shaders/fs.glsl");
#ifdef OUTLINE
        m_outlineSP = ShaderProgram("../../SphereMesh/shaders/outline_vs.glsl",
                                    "../../SphereMesh/shaders/outline_fs.glsl");
#endif

        m_camera.Setup(
                float(800) / 600,
                60.0f,
                1.0f,
                100.0f);
        m_camera.transform().position() = Vec3f(0.0f, 0.0f, 10.0f);

        // 需要从z轴向上，左手坐标系变换到y轴向上，右手坐标系
        // 1. 先将forward(在局部坐标系里是y)轴取反，可以将左手变为右手
        // 2. 确定局部坐标系的坐标轴在世界坐标系中的位置，实际上就是把y/z轴交换下位置，局部坐标系的y轴在世界坐标系中
        // 的位置是(0, 0, -1)
        // 矩阵的每一列实际上就对应局部坐标系xyz轴在世界坐标中的坐标表示
        // x轴在世界坐标中是(1, 0, 0)
        // y轴在世界坐标中是(0, 0, -1)
        // z轴在世界坐标中是(0, 1, 0)
        m_perDrawBuffer.modelMatrix = Mat4x4f(1.0f, 0.0f, 0.0f, 0.0f,
                                0.0f, 0.0f, 1.0f, 0.0f,
                                0.0f, -1.0f, 0.0f, 0.0f,
                                0.0f, 0.0f, 0.0f, 1.0f);

        // DirectionLight
        // 同unity的默认DirectionLight颜色
        m_dirLight.color = Vec3f(1.0f, 244.0f / 255.0f, 214.0f / 255.0f);
        // 同unity的默认DirectionLight方向(x:50 y:-30) 顺序zxy
        float thetaX = -50 * Deg2Rad;
        Mat4x4f rx = Mat4x4f(1.0f, 0.0f,         0.0f,          0.0f,
                             0.0f, cosf(thetaX),      -sinf(thetaX),      0.0f,
                             0.0f, sinf(thetaX),      cosf(thetaX),       0.0f,
                             0.0f, 0.0f,         0.0f,          1.0f);
        float thetaY = 210 * Deg2Rad;
        Mat4x4f ry = Mat4x4f(cosf(thetaY), 0.0f, sinf(thetaY), 0.0f,
                             0.0f,    1.0f, 0.0f,     0.0f,
                             -sinf(thetaY), 0.0f, cosf(thetaY),  0.0f,
                             0.0f,    0.0f, 0.0f,     1.0f);
        Vec3f dir;
        (rx * ry).MulVec(Vec3f(0.0f, 0.0f, 1.0f), dir);
        m_dirLight.direction = -dir;

        // PointLight
        // 同款unity配置
        m_pointLight.color = Vec3f(1.0f, 0.0f, 0.0f);
        m_pointLight.position = Vec3f(-8.0f, 8.0f, 8.0f); // 球的左上角
        m_pointLight.range = 20.0f; //超过这个范围的 直接认为0

        // SpotLight
        // 直接朝z负方向照过qu
        // 同款unity配置
        m_spotLight.color = Vec3f(0.0f, 1.0f, 0.0f);
        m_spotLight.range = 10.0f;
        m_spotLight.position = Vec3f(0.0f, 0.0f, 8.0f);
        m_spotLight.innerCos = cosf(1.0f * Deg2Rad);
        m_spotLight.direction = -Vec3f(0.0f, 0.0f, -1.0f);
        m_spotLight.outerCos = cosf(20.0f * Deg2Rad);

        // Material
        m_material.diffuseColor = Vec3f(128.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f);
        m_material.specularColor = Vec3f(1.0f, 1.0f, 1.0f);
        m_material.specularStrength = 100.0f;

        // 背面剔除
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        // 模板测试
        glEnable(GL_STENCIL_TEST);
        // 深度测试
        glEnable(GL_DEPTH_TEST);
        // 渲染为线框图
        //glPolygonMode(GL_FRONT, GL_LINE);

        // vertex array object
        glCreateVertexArrays(1, &m_vao);
        glBindVertexArray(m_vao);

        // element buffer object
        glCreateBuffers(1, &m_ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount * sizeof(GLuint), indices, GL_STATIC_DRAW);

        // vertex buffer object
        glCreateBuffers(1, &m_vbo);
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
        glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertices, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(Vec3f)));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(2 * sizeof(Vec3f)));
        glEnableVertexAttribArray(2);

        // textures
        int width, height, channel;
        stbi_uc* data = nullptr;
        stbi_set_flip_vertically_on_load(true);
        data = stbi_load("../../SphereMesh/textures/diffuse.png", &width, &height, &channel, 0);
        glCreateTextures(GL_TEXTURE_2D, 1, &m_diffuseTex);
        glBindTextureUnit(0, m_diffuseTex);
        // 上面这个基本等同于下面
//        glActiveTexture(GL_TEXTURE0 + 0);
//        glBindTexture(GL_TEXTURE_2D, m_diffuseTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
        stbi_image_free(data);
        data = stbi_load("../../SphereMesh/textures/specular.png", &width, &height, &channel, 0);
        glCreateTextures(GL_TEXTURE_2D, 1, &m_specularTex);
//        glBindTextureUnit(1, m_specularTex);
        // 上面这个基本等同于下面
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, m_specularTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
        stbi_image_free(data);
        data = nullptr;

        // per-draw ubo
        glCreateBuffers(1, &m_perDrawUBO);
        glNamedBufferData(m_perDrawUBO, sizeof(PerDrawBuffer), nullptr, GL_STATIC_DRAW);
//        glNamedBufferSubData(m_perDrawUBO, 0,
//                             sizeof(m_perDrawBuffer.modelMatrix),
//                             m_perDrawBuffer.modelMatrix.Transposed().data());
//        glNamedBufferSubData(m_perDrawUBO,
//                             sizeof(m_perDrawBuffer.modelMatrix),
//                             sizeof(m_perDrawBuffer.viewMatrix),
//                             m_perDrawBuffer.viewMatrix.Transposed().data());
//        glNamedBufferSubData(m_perDrawUBO,
//                             sizeof(m_perDrawBuffer.modelMatrix) + sizeof(m_perDrawBuffer.viewMatrix),
//                             sizeof(m_perDrawBuffer.projMatrix),
//                             m_perDrawBuffer.projMatrix.Transposed().data());
        glBindBufferBase(GL_UNIFORM_BUFFER, 0, m_perDrawUBO);

        // dir light ubo
        glCreateBuffers(1, &m_dirLightUBO);
        glNamedBufferData(m_dirLightUBO, sizeof(m_dirLight) + 2 * sizeof(GLfloat), nullptr, GL_STATIC_DRAW);
        GLubyte* ptr = static_cast<GLubyte*>(glMapNamedBuffer(m_dirLightUBO, GL_WRITE_ONLY));
        memcpy(ptr, &m_dirLight.color, sizeof(Vec3f));
        ptr += 4 * sizeof(GLfloat);
        memcpy(ptr, &m_dirLight.direction, sizeof(Vec3f));
        glUnmapNamedBuffer(m_dirLightUBO);
        glBindBufferBase(GL_UNIFORM_BUFFER, 1, m_dirLightUBO);

        // point light ubo
        glCreateBuffers(1, &m_pointLightUBO);
        glNamedBufferData(m_pointLightUBO, 8 * sizeof(GLfloat), nullptr, GL_STATIC_DRAW);
        ptr = static_cast<GLubyte*>(glMapNamedBuffer(m_pointLightUBO, GL_WRITE_ONLY));
        memcpy(ptr, &m_pointLight.color, sizeof(Vec3f));
        ptr += 4 * sizeof(GLfloat);
        memcpy(ptr, &m_pointLight.position, sizeof(Vec3f));
        ptr += sizeof(Vec3f);
        memcpy(ptr, &m_pointLight.range, sizeof(GLfloat));
        glUnmapNamedBuffer(m_pointLightUBO);
        glBindBufferBase(GL_UNIFORM_BUFFER, 2, m_pointLightUBO);

        // spot light ubo
        glCreateBuffers(1, &m_spotLightUBO);
        glNamedBufferData(m_spotLightUBO, sizeof(SpotLight), &m_spotLight, GL_STATIC_DRAW);
        glBindBufferBase(GL_UNIFORM_BUFFER, 3, m_spotLightUBO);

        // material ubo
        glCreateBuffers(1, &m_perMaterialUBO);
        glNamedBufferData(m_perMaterialUBO, 8 * sizeof(GLfloat), nullptr, GL_STATIC_DRAW);
        ptr = static_cast<GLubyte*>(glMapNamedBuffer(m_perMaterialUBO, GL_WRITE_ONLY));
        memcpy(ptr, &m_material.diffuseColor, sizeof(Vec3f));
        ptr += 4 * sizeof(GLfloat);
        memcpy(ptr, &m_material.specularColor, sizeof(Vec3f));
        ptr += sizeof(Vec3f);
        memcpy(ptr, &m_material.specularStrength, sizeof(GLfloat));
        glUnmapNamedBuffer(m_perMaterialUBO);
        glBindBufferBase(GL_UNIFORM_BUFFER, 10, m_perMaterialUBO);

        delete[] vertices;
        delete[] indices;
    }

    void OnRender() override
    {
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

        m_perDrawBuffer.viewMatrix = m_camera.worldToCameraMatrix();
        m_perDrawBuffer.projMatrix = m_camera.projectionMatrix();
        m_perDrawBuffer.cameraPos = m_camera.transform().position();
        GLubyte* ptr = static_cast<GLubyte*>(glMapNamedBuffer(m_perDrawUBO, GL_WRITE_ONLY));
        memcpy(ptr, m_perDrawBuffer.modelMatrix.Transposed().data(), sizeof(m_perDrawBuffer.modelMatrix));
        ptr += sizeof(m_perDrawBuffer.modelMatrix);
        memcpy(ptr, m_perDrawBuffer.viewMatrix.Transposed().data(), sizeof(m_perDrawBuffer.viewMatrix));
        ptr += sizeof(m_perDrawBuffer.viewMatrix);
        memcpy(ptr, m_perDrawBuffer.projMatrix.Transposed().data(), sizeof(m_perDrawBuffer.projMatrix));
        ptr += sizeof(m_perDrawBuffer.projMatrix);
        memcpy(ptr, &m_perDrawBuffer.cameraPos, sizeof(Vec3f));
        glUnmapNamedBuffer(m_perDrawUBO);

        glBindVertexArray(m_vao);

#ifdef OUTLINE
        glStencilFunc(GL_ALWAYS, 0x1, 0xff);
        glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
#endif
        m_sp.Use();
        glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, nullptr);

#ifdef OUTLINE
        glStencilFunc(GL_EQUAL, 0x0, 0xff);
        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
        m_outlineSP.Use();
        glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, nullptr);
#endif
    }

    void OnDestroy() override
    {
        glDeleteVertexArrays(1, &m_vao);
        glDeleteBuffers(1, &m_ebo);
        glDeleteBuffers(1, &m_vbo);
        glDeleteTextures(1, &m_diffuseTex);
    }

    void OnKey(int keyCode, int action, int mods) override
    {
        // 上下左右移动摄像机
        float forwardOffset = 0.0f;
        float rightOffset = 0.0f;
        switch (keyCode)
        {
            case GLFW_KEY_W:
                forwardOffset -= 0.1f;
                break;
            case GLFW_KEY_S:
                forwardOffset += 0.1f;
                break;
            case GLFW_KEY_A:
                rightOffset -= 0.1f;
                break;;
            case GLFW_KEY_D:
                rightOffset += 0.1f;
                break;
        }

        Vec3f forward = m_camera.transform().forward();
        Vec3f right = m_camera.transform().right();
        m_camera.transform().position() += forward * forwardOffset;
        m_camera.transform().position() += right * rightOffset;
    }

private:
    GLuint m_vao;
    GLuint m_vbo;
    GLuint m_ebo;
    GLuint m_diffuseTex;
    GLuint m_specularTex;
    GLuint m_perDrawUBO;
    GLuint m_perMaterialUBO;
    GLuint m_dirLightUBO;
    GLuint m_pointLightUBO;
    GLuint m_spotLightUBO;
    int m_indexCount;
    ShaderProgram m_sp = {};
#ifdef OUTLINE
    ShaderProgram m_outlineSP = {};
#endif
    Camera m_camera = {};
    PerDrawBuffer m_perDrawBuffer;
    DirLight m_dirLight;
    PointLight m_pointLight;
    SpotLight m_spotLight;
    Material m_material;
};


int main()
{
    try
    {
        App().Run();
    }
    catch(const GLException& e)
    {
        std::cout << e.msg() << std::endl;
    }
    return 0;
}